/**************************************************************************
 *
 * Copyright (C) 2004 Steve Karg
 *
 * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0
 *
 *********************************************************************/
/** @file win32/rs485.c  Provides Windows-specific functions for RS-485 */

/* Suggested USB to RS485 devices:
   B&B Electronics USOPTL4
   SerialGear USB-COMi-SI-M
   USB-RS485-WE-1800-BT
*/

#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include "bacnet/datalink/mstp.h"
#include "bacnet/datalink/dlmstp.h"
#define WIN32_LEAN_AND_MEAN
#define STRICT 1
#include <windows.h>
#include "rs485.h"
#include "bacnet/basic/sys/fifo.h"

/* details from Serial Communications in Win32 at MSDN */

/* Win32 handle for the port */
HANDLE RS485_Handle;
/* Original COM Timeouts */
static COMMTIMEOUTS RS485_Timeouts;
/* COM port name COM1, COM2, etc  */
static char RS485_Port_Name[256] = "COM4";
/* baud rate - MS enumerated
    CBR_110, CBR_300, CBR_600, CBR_1200, CBR_2400,
    CBR_4800, CBR_9600, CBR_14400, CBR_19200, CBR_38400,
    CBR_56000, CBR_57600, CBR_115200, CBR_128000, CBR_256000 */
static DWORD RS485_Baud = CBR_38400;
/* ByteSize in bits: 5, 6, 7, 8 are valid */
static DWORD RS485_ByteSize = 8;
/* Parity - MS enumerated:
    NOPARITY, EVENPARITY, ODDPARITY, MARKPARITY, SPACEPARITY */
static DWORD RS485_Parity = NOPARITY;
/* StopBits - MS enumerated:
    ONESTOPBIT, ONE5STOPBITS, TWOSTOPBITS */
static DWORD RS485_StopBits = ONESTOPBIT;
/* DTRControl - MS enumerated:
    DTR_CONTROL_ENABLE, DTR_CONTROL_DISABLE, DTR_CONTROL_HANDSHAKE */
static DWORD RS485_DTRControl = DTR_CONTROL_DISABLE;
/* RTSControl - MS enumerated:
    RTS_CONTROL_ENABLE, RTS_CONTROL_DISABLE,
    RTS_CONTROL_HANDSHAKE, RTS_CONTROL_TOGGLE */
static DWORD RS485_RTSControl = RTS_CONTROL_DISABLE;

/****************************************************************************
 * DESCRIPTION: Change the characters in a string to uppercase
 * RETURN:      nothing
 * ALGORITHM:   none
 * NOTES:       none
 *****************************************************************************/
static void strupper(char *str)
{
    char *p;
    for (p = str; *p != '\0'; ++p) {
        *p = (char)toupper(*p);
    }
}

/****************************************************************************
 * DESCRIPTION: Initializes the RS485 hardware and variables, and starts in
 *              receive mode.
 * RETURN:      none
 * ALGORITHM:   none
 * NOTES:       expects a constant char ifname, or char from the heap
 *****************************************************************************/
void RS485_Set_Interface(char *ifname)
{
    /* For COM ports greater than 9 you have to use a special syntax
       for CreateFileA. The syntax also works for COM ports 1-9. */
    /* http://support.microsoft.com/kb/115831 */
    if (ifname) {
        strupper(ifname);
        if (strncmp("COM", ifname, 3) == 0) {
            if (strlen(ifname) > 3) {
                snprintf(
                    RS485_Port_Name, sizeof(RS485_Port_Name), "\\\\.\\COM%i",
                    atoi(ifname + 3));
                fprintf(
                    stdout, "Adjusted interface name to %s\r\n",
                    RS485_Port_Name);
            }
        }
    }
}

/****************************************************************************
 * DESCRIPTION: Check the serial port to see if port exists
 * RETURN:      true if port exists
 * ALGORITHM:   none
 * NOTES:       none
 *****************************************************************************/
bool RS485_Interface_Valid(unsigned port_number)
{
    HANDLE h = 0;
    DWORD err = 0;
    bool status = false;
    char ifname[255] = "";

    snprintf(ifname, sizeof(ifname), "\\\\.\\COM%u", port_number);
    h = CreateFileA(
        ifname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    if (h == INVALID_HANDLE_VALUE) {
        err = GetLastError();
        if ((err == ERROR_ACCESS_DENIED) || (err == ERROR_GEN_FAILURE) ||
            (err == ERROR_SHARING_VIOLATION) || (err == ERROR_SEM_TIMEOUT)) {
            status = true;
        }
    } else {
        status = true;
        CloseHandle(h);
    }

    return status;
}

const char *RS485_Interface(void)
{
    return RS485_Port_Name;
}

void RS485_Print_Error(void)
{
    LPVOID lpMsgBuf;

    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL,
        GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpMsgBuf, 0, NULL);
    MessageBox(NULL, lpMsgBuf, "GetLastError", MB_OK | MB_ICONINFORMATION);
    LocalFree(lpMsgBuf);

    return;
}

static void RS485_Configure_Status(void)
{
    DCB dcb = { 0 };
    COMMTIMEOUTS ctNew;

    dcb.DCBlength = sizeof(dcb);
    /* get current DCB settings */
    if (!GetCommState(RS485_Handle, &dcb)) {
        fprintf(stderr, "Unable to get status from %s\n", RS485_Port_Name);
        RS485_Print_Error();
        exit(1);
    }

    /* update DCB rate, byte size, parity, and stop bits size */
    dcb.BaudRate = RS485_Baud;
    dcb.ByteSize = (unsigned char)RS485_ByteSize;
    dcb.Parity = (unsigned char)RS485_Parity;
    dcb.StopBits = (unsigned char)RS485_StopBits;

    /* update flow control settings */
    dcb.fDtrControl = RS485_DTRControl;
    dcb.fRtsControl = RS485_RTSControl;
    /*
       dcb.fOutxCtsFlow    = CTSOUTFLOW(TTYInfo);
       dcb.fOutxDsrFlow    = DSROUTFLOW(TTYInfo);
       dcb.fDsrSensitivity = DSRINFLOW(TTYInfo);
       dcb.fOutX           = XONXOFFOUTFLOW(TTYInfo);
       dcb.fInX            = XONXOFFINFLOW(TTYInfo);
       dcb.fTXContinueOnXoff = TXAFTERXOFFSENT(TTYInfo);
       dcb.XonChar         = XONCHAR(TTYInfo);
       dcb.XoffChar        = XOFFCHAR(TTYInfo);
       dcb.XonLim          = XONLIMIT(TTYInfo);
       dcb.XoffLim         = XOFFLIMIT(TTYInfo);
       // DCB settings not in the user's control
       dcb.fParity = TRUE;
     */
    if (!SetCommState(RS485_Handle, &dcb)) {
        fprintf(stderr, "Unable to set status on %s\n", RS485_Port_Name);
        RS485_Print_Error();
    }
    /* configure the time-out parameters for a communications device. */
    /* If an application sets ReadIntervalTimeout and
       ReadTotalTimeoutMultiplier to MAXDWORD and
       sets ReadTotalTimeoutConstant to a value greater
       than zero and less than MAXDWORD, one of the following
       occurs when the ReadFile function is called:
        * If there are any bytes in the input buffer,
          ReadFile returns immediately with the bytes in the buffer.
        * If there are no bytes in the input buffer,
          ReadFile waits until a byte arrives and then returns immediately.
        * If no bytes arrive within the time specified
          by ReadTotalTimeoutConstant, ReadFile times out.

        Constant values are in milliseconds
     */
    ctNew.ReadIntervalTimeout = MAXDWORD;
    ctNew.ReadTotalTimeoutMultiplier = MAXDWORD;
    ctNew.ReadTotalTimeoutConstant = 1;
    ctNew.WriteTotalTimeoutMultiplier = 0;
    ctNew.WriteTotalTimeoutConstant = 0;
    if (!SetCommTimeouts(RS485_Handle, &ctNew)) {
        RS485_Print_Error();
    }
    /* Get rid of any stray characters */
    if (!PurgeComm(RS485_Handle, PURGE_TXABORT | PURGE_RXABORT)) {
        fprintf(stderr, "Unable to purge %s\n", RS485_Port_Name);
        RS485_Print_Error();
    }
    /* Set the Comm buffer size */
    SetupComm(RS485_Handle, DLMSTP_MPDU_MAX, DLMSTP_MPDU_MAX);
    /* raise DTR */
    if (!EscapeCommFunction(RS485_Handle, SETDTR)) {
        fprintf(stderr, "Unable to set DTR on %s\n", RS485_Port_Name);
        RS485_Print_Error();
    }
}

/****************************************************************************
 * DESCRIPTION: Cleans up any handles that were created at startup.
 * RETURN:      none
 * ALGORITHM:   none
 * NOTES:       none
 *****************************************************************************/
static void RS485_Cleanup(void)
{
    if (!EscapeCommFunction(RS485_Handle, CLRDTR)) {
        RS485_Print_Error();
    }

    if (!SetCommTimeouts(RS485_Handle, &RS485_Timeouts)) {
        RS485_Print_Error();
    }

    CloseHandle(RS485_Handle);
}

/****************************************************************************
 * DESCRIPTION: Initializes the RS485 hardware and variables, and starts in
 *              receive mode.
 * RETURN:      none
 * ALGORITHM:   none
 * NOTES:       none
 *****************************************************************************/
void RS485_Initialize(void)
{
    RS485_Handle = CreateFileA(
        RS485_Port_Name, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING,
        /*FILE_FLAG_OVERLAPPED */ 0, 0);
    if (RS485_Handle == INVALID_HANDLE_VALUE) {
        DWORD err = GetLastError();
        fprintf(
            stderr, "RS485 unable to open %s (Error %lu)\n", RS485_Port_Name,
            err);
        RS485_Print_Error();
        exit(1);
    }
    if (!GetCommTimeouts(RS485_Handle, &RS485_Timeouts)) {
        RS485_Print_Error();
    }
    RS485_Configure_Status();
#if PRINT_ENABLED
    fprintf(stdout, "RS485 Interface: %s\n", RS485_Port_Name);
    fprintf(stdout, "RS485 Baud Rate %u\n", RS485_Get_Baud_Rate());
    fflush(stdout);
#endif

    atexit(RS485_Cleanup);

    return;
}

/****************************************************************************
 * DESCRIPTION: Returns the baud rate that we are currently running at
 * RETURN:      none
 * ALGORITHM:   none
 * NOTES:       none
 *****************************************************************************/
uint32_t RS485_Get_Baud_Rate(void)
{
    switch (RS485_Baud) {
        case CBR_19200:
            return 19200;
        case CBR_38400:
            return 38400;
        case CBR_57600:
            return 57600;
        case CBR_115200:
            return 115200;
        case CBR_110:
            return 110;
        case CBR_300:
            return 300;
        case CBR_600:
            return 600;
        case CBR_1200:
            return 1200;
        case CBR_2400:
            return 2400;
        case CBR_4800:
            return 4800;
        case CBR_14400:
            return 14400;
        case CBR_56000:
            return 56000;
        case CBR_128000:
            return 128000;
        case CBR_256000:
            return 256000;
        case 76800:
            /* See comments in RS485_Set_Baud_Rate() below
             * also look at definition of CBR_xx in winbase.h
             * some serial drivers will only support the defined
             * baud rates but others will try and configure the
             * requested baud rate (or as close as they can get)
             */
            return 76800;
        case CBR_9600:
        default:
            return 9600;
    }
}

/****************************************************************************
 * DESCRIPTION: Sets the baud rate for the chip USART
 * RETURN:      none
 * ALGORITHM:   none
 * NOTES:       none
 *****************************************************************************/
bool RS485_Set_Baud_Rate(uint32_t baud)
{
    bool valid = true;

    switch (baud) {
        case 9600:
            RS485_Baud = CBR_9600;
            break;
        case 19200:
            RS485_Baud = CBR_19200;
            break;
        case 38400:
            RS485_Baud = CBR_38400;
            break;
        case 57600:
            RS485_Baud = CBR_57600;
            break;
        case 115200:
            RS485_Baud = CBR_115200;
            break;
        case 110:
            RS485_Baud = CBR_110;
            break;
        case 300:
            RS485_Baud = CBR_300;
            break;
        case 600:
            RS485_Baud = CBR_600;
            break;
        case 1200:
            RS485_Baud = CBR_1200;
            break;
        case 2400:
            RS485_Baud = CBR_2400;
            break;
        case 4800:
            RS485_Baud = CBR_4800;
            break;
        case 14400:
            RS485_Baud = CBR_14400;
            break;
        case 56000:
            RS485_Baud = CBR_56000;
            break;
        case 128000:
            RS485_Baud = CBR_128000;
            break;
        case 256000:
            RS485_Baud = CBR_256000;
            break;
        case 76800:
            /* I'm using the B&B Electronics USOPTL4 USB RS485 adapter
             * on Win 7 and building with VS2008 Express Edition and it
             * seems to work for the most part if I use the following.
             * I get the occasional data errors especially if the devices
             * are transmitting with 1 stop bit (some devices receive with
             * 1 stop bit but effectivly end up transmitting with 2 stop
             * bits, usually because of synchroisation issues in some UARTs
             * which mean that if you wait until the serialiser has finished
             * with the current character and then load the TX buffer it has
             * to wait until the next bit boundary to start transmitting.
             * PMcS
             */
            RS485_Baud = 76800;
            break;
        default:
            valid = false;
            break;
    }

    if (valid) {
        /* FIXME: store the baud rate */
    }

    return valid;
}

/* Transmits a Frame on the wire */
void RS485_Send_Frame(
    struct mstp_port_struct_t *mstp_port, /* port specific data */
    const uint8_t *buffer, /* frame to send (up to 501 bytes of data) */
    uint16_t nbytes)
{ /* number of bytes of data (up to 501) */
    DWORD dwWritten = 0;

    if (mstp_port) {
        uint32_t baud;
        uint8_t turnaround_time;
        baud = RS485_Get_Baud_Rate();
        /* wait about 40 bit times since reception */
        if (baud == 9600) {
            turnaround_time = 4;
        } else if (baud == 19200) {
            turnaround_time = 2;
        } else {
            turnaround_time = 2;
        }
        while (mstp_port->SilenceTimer(NULL) < turnaround_time) {
            /* do nothing - wait for timer to increment */
        };
    }
    WriteFile(RS485_Handle, buffer, nbytes, &dwWritten, NULL);

    /* per MSTP spec, reset SilenceTimer after each byte is sent */
    if (mstp_port) {
        mstp_port->SilenceTimerReset(NULL);
    }

    return;
}

/* called by timer, interrupt(?) or other thread */
void RS485_Check_UART_Data(struct mstp_port_struct_t *mstp_port)
{
    char lpBuf[1];
    DWORD dwRead = 0;

    if (mstp_port->ReceiveError == true) {
        /* wait for state machine to clear this */
    }
    /* wait for state machine to read from the DataRegister */
    else if (mstp_port->DataAvailable == false) {
        /* check for data */
        if (!ReadFile(RS485_Handle, lpBuf, sizeof(lpBuf), &dwRead, NULL)) {
            if (GetLastError() != ERROR_IO_PENDING) {
                mstp_port->ReceiveError = TRUE;
            }
        } else {
            if (dwRead) {
                mstp_port->DataRegister = lpBuf[0];
                mstp_port->DataAvailable = TRUE;
            }
        }
    }
}

/*************************************************************************
 * Description: print available COM ports
 * Returns: none
 * Notes: none
 **************************************************************************/
void RS485_Print_Ports(void)
{
    unsigned i = 0;

    /* try to open all 255 COM ports */
    for (i = 1; i < 256; i++) {
        if (RS485_Interface_Valid(i)) {
            /* note: format for Wireshark ExtCap */
            printf(
                "interface {value=COM%u}"
                "{display=BACnet MS/TP on COM%u}\n",
                i, i);
        }
    }
}

#ifdef TEST_RS485

#include "bacnet/datalink/mstpdef.h"

static void test_transmit_task(void *pArg)
{
    char *TxBuf = "BACnet MS/TP";
    size_t len = strlen(TxBuf) + 1;

    while (TRUE) {
        Sleep(1000);
        RS485_Send_Frame(NULL, &TxBuf[0], len);
    }
}

#if defined(_WIN32)
static BOOL WINAPI CtrlCHandler(DWORD dwCtrlType)
{
    dwCtrlType = dwCtrlType;
    exit(0);
    return TRUE;
}
#endif

static int ascii_hex_to_int(char ch)
{
    int rv = -1;

    if ((ch >= '0') && (ch <= '9')) {
        rv = ch - '0';
    } else if ((ch >= 'a') && (ch <= 'f')) {
        rv = 10 + ch - 'a';
    } else if ((ch >= 'A') && (ch <= 'F')) {
        rv = 10 + ch - 'a';
    }

    return rv;
}

int main(int argc, char *argv[])
{
    unsigned long hThread = 0;
    uint32_t arg_value = 0;
    char lpBuf[1];
    DWORD dwRead = 0;
    unsigned i = 0, len = 0, count = 0;
    char hex_pair[5] = "0xff";
    char ch = ' ';
    int lsb = 0, msb = 0;
    long my_baud = 38400;
    uint8_t buffer[501] = { 0 };

    if (argc > 1) {
        RS485_Set_Interface(argv[1]);
    }
    if (argc > 2) {
        my_baud = strtol(argv[2], NULL, 0);
    }
    RS485_Set_Baud_Rate(my_baud);
    RS485_Initialize();
#if defined(_WIN32)
    SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_PROCESSED_INPUT);
    SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlCHandler, TRUE);
#endif
#ifdef TEST_RS485_TRANSMIT
    /* read a stream of characters from stdin or argument */
    if (argc > 3) {
        len = strlen(argv[3]);
        for (i = 0; i < len; i++) {
            /* grab pairs of hex characters, skip spaces */
            ch = argv[3][i];
            if (ch == ' ') {
                continue;
            }
            msb = ascii_hex_to_int(ch);
            if (msb >= 0) {
                i++;
                ch = argv[3][i];
                lsb = ascii_hex_to_int(ch);
                if (lsb >= 0) {
                    buffer[count] = msb << 4 | lsb;
                } else {
                    buffer[count] = msb;
                }
                count++;
                if (count >= sizeof(buffer)) {
                    break;
                }
            }
        }
        RS485_Send_Frame(NULL, buffer, count);
    }
#endif
#ifdef TEST_RS485_RECEIVE
    /* receive task */
    for (;;) {
        if (!ReadFile(RS485_Handle, lpBuf, sizeof(lpBuf), &dwRead, NULL)) {
            if (GetLastError() != ERROR_IO_PENDING) {
                RS485_Print_Error();
            }
        } else {
            /* print any characters received */
            if (dwRead) {
                for (i = 0; i < dwRead; i++) {
                    fprintf(stderr, "%02X ", lpBuf[i]);
                }
            }
            dwRead = 0;
        }
    }
#endif
}
#endif
